# View must implement the following:
#   - update(x, y, width=1, height=1)
#   - self.map
#   - self.cursor_pos = (x, y)
#   - self.cursor_direction = Directions.north
#   - self.text (property to get/set text in textarea)

from Tkinter import *
import tkSimpleDialog, tkFileDialog, tkMessageBox
import dumper
import re
import os

from TarjanObservable import TarjanObservable
from TarjanDirection import Directions

# CONSTANT, Size of a cell in pixels
DV = 25

class rect (object):
    def __init__(self, x, y, w, h, gridx, gridy):
        self.coords = ((x, y), (x + w, y + h))
        self.gridx = gridx
        self.gridy = gridy
    x = property(fget=lambda self: self.coords[0][0])
    y = property(fget=lambda self: self.coords[0][1])
    x2 = property(fget=lambda self: self.coords[1][0])
    y2 = property(fget=lambda self: self.coords[1][1])
    w = property(fget=lambda self: self.x2 - self.x)
    h = property(fget=lambda self: self.y2 - self.y)
    cx = property(fget=lambda self: self.x + self.w / 2)
    cy = property(fget=lambda self: self.y + self.h / 2)

class AutoScrollbar(Scrollbar):
    def set(self, lo, hi):
        if float(lo) <= 0.0 and float(hi) >= 1.0:
            self.tk.call("grid", "remove", self)
        else:
            self.grid()
        Scrollbar.set(self, lo, hi)

class WidthHeightDialog(tkSimpleDialog.Dialog):
    def __init__(self, parent, title, width, height):
        self.width = StringVar(value=width)
        self.height = StringVar(value=height)
        self.success = None
        tkSimpleDialog.Dialog.__init__(self, parent, title)

    def body(self, master):
        Label(master, text="Width:").grid(row=0, sticky=W)
        Label(master, text="Height:").grid(row=1, sticky=W)

        width_entry = Entry(master, textvariable=self.width)
        height_entry = Entry(master, textvariable=self.height)

        width_entry.grid(row=0, column=1)
        height_entry.grid(row=1, column=1)

        width_entry.selection_range(0, END)
        return width_entry # initial focus

    def apply(self):
        self.success = int(self.width.get()), int(self.height.get())


class TarjanView(TarjanObservable):

    def __init__(self):
        TarjanObservable.__init__(self)

        # initialize cursor variables self.map = None
        self.filename = None
        self.path = "."
        self.cursor_pos = (0, 0)
        self.cursor_direction = Directions.south
        self.marked_area = None

        # intialize Tkinter and create window
        self.root = Tk()
        self.root.protocol("WM_DELETE_WINDOW", self.quit)

        m = PanedWindow(self.root)
        m.pack(fill=BOTH, expand=1)

        # create map frame with canvas and scrollbars
        frame = Frame(m)
        frame.grid_rowconfigure(0, weight=1)
        frame.grid_columnconfigure(0, weight=1)

        xscrollbar = AutoScrollbar(frame, orient=HORIZONTAL)
        xscrollbar.grid(row=1, column=0, sticky=E+W)
        yscrollbar = AutoScrollbar(frame)
        yscrollbar.grid(row=0, column=1, sticky=N+S)

        self.canvas = Canvas(frame,
            bd=0,
            xscrollcommand=xscrollbar.set,
            yscrollcommand=yscrollbar.set,
            background="lightgrey"
        )
        self.canvas.grid(row=0, column=0, sticky=N+S+E+W)

        xscrollbar.config(command=self.canvas.xview)
        yscrollbar.config(command=self.canvas.yview)

        # add map frame to window
        m.add(frame)
        #frame.pack(fill=BOTH, expand=1)

        # create menu
        self.menubar = Menu(self.root)
        filemenu = Menu(self.menubar, tearoff=0)
        filemenu.add_command(label="New", command=self.new_map)
        filemenu.add_command(label="Open", command=self.open_map)
        filemenu.add_command(label="Save", command=self.save_map)
        filemenu.add_command(label="Save As..", command=self.save_map_as)
        filemenu.add_separator()
        filemenu.add_command(label="Quit", command=self.quit)
        self.root.bind("<Command-q>", lambda x: self.quit)

        editmenu = Menu(self.menubar, tearoff=0)
        editmenu.add_command(label="Resize", command=self.resize_map)
        filemenu.add_separator()
        editmenu.add_checkbutton(label="Flipped X coordinate")
        editmenu.add_checkbutton(label="Flipped Y coordinate")
        self.menubar.add_cascade(label="File", menu=filemenu)
        self.menubar.add_cascade(label="Edit", menu=editmenu)

        # add menu to window
        self.root.config(menu = self.menubar)

        # create textview and add it to window.
        textframe = Frame(m)
        textframe.grid_rowconfigure(0, weight=1)
        textframe.grid_columnconfigure(0, weight=1)
        text_yscrollbar = AutoScrollbar(textframe)
        text_yscrollbar.grid(row=0, column=1, sticky=N+S)

        self.textarea = Text(textframe, wrap=WORD, yscrollcommand=text_yscrollbar.set)
        self.textarea.grid(row=0, column=0, sticky=N+S+E+W)

        text_yscrollbar.config(command=self.textarea.yview)

        m.add(textframe)

        # add keyboard handlers
        self.defineMenuHotkeys()
        self.canvas.bind("<Key>", self.keyPressed_handler)
        self.textarea.bind("<Tab>", lambda e: self.canvas.focus_set() or 'break') # force return of 'break'

        # set focus to canvas
        self.canvas.focus_set()

    def defineMenuHotkeys(self):
        if os.sys.platform == "darwin":
            self.root.bind("<Command-q>", lambda x: self.quit())
            self.root.bind("<Command-Shift-s>", lambda x: self.save_map_as())
            self.root.bind("<Command-s>", lambda x: self.save_map())
            self.root.bind("<Command-n>", lambda x: self.new_map())
            self.root.bind("<Command-o>", lambda x: self.open_map())
            

    def run(self):
        self.root.mainloop()

    def get_text(self):
        return self.textarea.get(0.0, END)

    def set_text(self, text):
        self.textarea.delete(0.0, END)
        self.textarea.insert(END, text)

    text = property(get_text, set_text)

    def cell_to_rect(self, x, y):
        "create a rect corresponding to cell x, y in map." 
        return rect(x * (DV + 1) + DV / 2,
            y * (DV + 1) + DV / 1,
            DV + 1,
            DV + 1,
            x,
            y
        )

    def update(self, _x, _y, _w=1, _h=1):

        for cell in [self.map.getCell(x, y) for y in xrange(_y, _y + _h) for x in xrange(_x, _x + _w)]:

            rect = self.cell_to_rect(cell.x, cell.y)

            dx = rect.w / 2; dy = rect.h / 2;
            sdx = rect.w / 4; sdy = rect.h / 4

            # clear out cell
            for item in self.canvas.find_enclosed(rect.x - dx, rect.y - dy, rect.x2 + dx, rect.y2 + dy):
                self.canvas.delete(item)

            # draw floor
            getattr(self, 'drawFloor_' + cell.floor, self.drawFloor_unknown)(cell, rect)

            # draw walls
            draw_north = getattr(self, 'drawWall_' + cell.north, self.drawWall_empty)
            draw_north( rect.x, rect.y, rect.x2, rect.y )

            draw_east = getattr(self, 'drawWall_' + cell.east, self.drawWall_empty)
            draw_east( rect.x2, rect.y, rect.x2, rect.y2 )

            draw_south = getattr(self, 'drawWall_' + cell.south, self.drawWall_empty)
            draw_south( rect.x, rect.y2, rect.x2, rect.y2 )

            draw_west = getattr(self, 'drawWall_' + cell.west, self.drawWall_empty)
            draw_west( rect.x, rect.y, rect.x, rect.y2 )

            # draw cursor
            if (self.cursor_pos == (cell.x, cell.y)):
                if self.cursor_direction == Directions.north:
                    self.drawCursor(rect.x + dx, rect.y + sdy - 4,
                                    rect.x + sdx, rect.y + 3 * sdx,
                                    rect.x + 3 * sdx, rect.y + 3 * sdx)
                elif self.cursor_direction == Directions.east:
                    self.drawCursor(rect.x + 3 * sdx + 4, rect.y + dy,
                                    rect.x + sdx, rect.y + 3 * sdx,
                                    rect.x + sdx, rect.y + sdx)
                elif self.cursor_direction == Directions.south:
                    self.drawCursor(rect.x + dx, rect.y + 3 * sdy + 4,
                                    rect.x + sdx, rect.y + sdx,
                                    rect.x + 3 * sdx, rect.y + sdx)
                elif self.cursor_direction == Directions.west:
                    self.drawCursor(rect.x + sdx - 4, rect.y + dy,
                                    rect.x + 3 * sdx, rect.y + 3 * sdx,
                                    rect.x + 3 * sdx, rect.y + sdx)

            self.drawNotes(rect, cell.notes)

        # update scrollbars, if required
        if not tuple(map(int, self.canvas.config('scrollregion')[-1])) == self.canvas.bbox(ALL):
            self.canvas.config(scrollregion=self.canvas.bbox(ALL))
        
        if self.filename is None:
            title = 'Tarjan - unnammed'
        else:
            title = 'Tarjan - ' + self.filename
        self.root.title(title + ( self.map.tainted and '[*]' or ''))

# ============================= FLOORS AND WALLS =================================

    def drawNotes(self, rect, notes):
        self.canvas.create_text((rect.cx, rect.cy), font="Courier 7", fill="blue", width=rect.w, text=notes)

    def drawCursor(self, x, y, x2, y2, x3, y3):
        self.canvas.create_polygon(x, y, x2, y2, x3, y3, fill="green")

    def drawFloor_unknown(self, cell, rect):
        return self.canvas.create_rectangle(rect.coords, fill="", **self.get_outline(rect))

    def drawFloor_empty(self, cell, rect):
        return self.canvas.create_rectangle(rect.coords, fill="white", **self.get_outline(rect))

    def drawFloor_darkness(self, cell, rect):
        return self.canvas.create_rectangle(rect.coords, fill="darkgrey", **self.get_outline(rect))

    def drawFloor_stairs_up(self, cell, rect):
        pass #TODO

    def drawFloor_stairs_down(self, cell, rect):
        pass #TODO

    def drawWall_empty(self, x, y, x2, y2):
        l = int( round(x == x2 and y2 - y or x2 - x) )
        self.canvas.create_line(x, y, x2, y2, dash=(1, l - 2 , 1), capstyle=ROUND)
        
    def drawWall_wall(self, x, y, x2, y2):
        self.canvas.create_line(x, y, x2, y2)

    def drawWall_door(self, x, y, x2, y2):
        l = int( round( (x == x2 and y2 - y or x2 - x) / 3) )
        self.canvas.create_line(x, y, x2, y2, dash=(l + 1, l, l + 1))

    def drawWall_gate(self, x, y, x2, y2):
        dx = x == x2 and 1 or 0; dy = y == y2 and 1 or 0
        self.canvas.create_line(x - dx, y - dy, x2 - dx, y2 - dy)
        self.canvas.create_line(x + dx, y + dy, x2 + dx, y2 + dy)

    def drawWall_secret(self, x, y, x2, y2):
        self.canvas.create_line(x, y, x2, y2)
        self.canvas.create_text(x + (x2 - x) / 2, y + (y2 - y) / 2, text="S")

    def drawWall_locked_door(self, x, y, x2, y2):
        self.canvas.create_line(x, y, x2, y2)
        if (x == x2):
            _y = y + int(round((y2 - y) / 2))
            self.canvas.create_line(x - 3 , _y, x + 4, _y)
        if (y == y2):
            _x = x + int(round((x2 - x) / 2))
            self.canvas.create_line(_x, y - 3, _x, y + 4)

    def drawWall_arch(self, x, y, x2, y2):
        pass # TODO

    def get_outline(self, rect):
        if (self.marked_area is not None
        and self.marked_area[0] <= rect.gridx <= self.marked_area[0] + self.marked_area[2]
        and self.marked_area[1] <= rect.gridy <= self.marked_area[1] + self.marked_area[3]):
            return { "width": 3, "outline": "green" }
        else:
            return { "width": 1, "outline": "" }
            

# ================================================================================

# ============================ COMMANDS AND EVENTS ===============================

    def keyPressed_handler(self, event):
        #dumper.dump(event.state)
        if event.char != event.keysym:
            key = "<"
            if event.state & 1:
                key = key + "SHIFT-"
            if event.state & 4:
                key = key + "CTRL-"
            self.notify(key + event.keysym.upper() + ">")
        elif event.state == 0:
            if event.char == 'n':
                self.set_note()
            elif event.char == 'g':
                self.move_to()
            elif event.char == 'm':
                self.notify("<<EnterMarkMode>>")
            else:
                self.notify(event.char)

    def new_map(self):
        popup = WidthHeightDialog(self.root, "New map", self.map.width, self.map.height)
        if popup.success:
            self.filename = None
            self.canvas.delete(ALL)
            self.text = ""
            self.notify("<<NewMap>>", *popup.success)
        self.canvas.focus_force()

    def resize_map(self):
        popup = WidthHeightDialog(self.root, "Resize map", self.map.width, self.map.height)
        if popup.success:
            self.canvas.delete(ALL)
            self.notify("<<ResizeMap>>", *popup.success)
        self.canvas.focus_force()

    def open_map(self):
        filename = tkFileDialog.askopenfilename(
                title="Save map",
                initialdir= self.path,
                #defaultextension='.tarjanmap',
                filetypes=(('Tarjan map file', '.tarjanmap'),),
            )
        if filename:
            self.canvas.delete(ALL)
            self.filename = os.path.basename(filename)
            self.path = os.path.dirname(filename)
            self.notify("<<OpenMap>>", filename)

        self.canvas.focus_force()

    def save_map(self):
        if self.filename == None:
            self.save_map_as()
        else:
            self.notify("<<SaveMap>>", self.path + '/' + self.filename)
            
    def save_map_as(self):

        filename = tkFileDialog.asksaveasfilename(
                title="Save map",
                defaultextension='.tarjanmap',
                filetypes=(('Tarjan map file', '.tarjanmap'),),
                initialfile=self.filename,
                initialdir=self.path,
            )
        if filename:
            self.filename = os.path.basename(filename)
            self.path = os.path.dirname(filename)
            self.notify("<<SaveMap>>", filename)
        self.canvas.focus_force()

    def quit(self):
        if self.map.tainted:
            if tkMessageBox.askokcancel("Discard changes?", "There are unsaved changes - are you sure you want to quit?"):
                self.root.quit()
        else:
            self.root.quit()


    def set_note(self):
        notes = tkSimpleDialog.askstring(
                "Enter notes",
                "Set notes for %d, %d" % (self.cursor_pos),
                initialvalue=self.map.getCell(*self.cursor_pos).notes )
        if not notes is None:
            self.notify("<<SetNote>>", notes)
        self.canvas.focus_force()

    def move_to(self):
        coords = tkSimpleDialog.askstring("Move to", "Enter coordinates (eg. 5,5)")
        if not coords is None: 
            match = re.match("\s*(\d+)\s*,\s*(\d+)", coords)
            if match:
                self.notify("<<GoTo>>", map(int, match.groups()))
        self.canvas.focus_force()
# ================================================================================

if __name__ == "__main__":

    map = TarjanMap()

    #init map to some testmap
    for cell in [map.getCell(x, y) for y in xrange(1) for x in xrange(map.width)]:
        cell.floor = 'empty'
    for cell in [map.getCell(x, y) for y in xrange(1, map.height - 1) for x in xrange(0, map.width, map.width - 1)]:
        cell.floor = 'empty'
    for cell in [map.getCell(x, y) for y in xrange(map.height - 1, map.height) for x in xrange(map.width)]:
        cell.floor = 'empty'

    map.getCell(5, 5).east = 'wall'
    map.getCell(5, 5).west = 'wall'
    map.getCell(5, 5).north = 'door'
    map.getCell(5, 6).east = 'gate'
    map.getCell(5, 7).east = 'wall'
    map.getCell(5, 7).south = 'wall'
    map.getCell(5, 7).west = 'wall'
    map.getCell(5, 7).floor = 'empty'
    map.getCell(4, 6).west = 'secret'
    map.getCell(4, 6).north = 'wall'
    map.getCell(4, 6).south = 'wall'

    map.getCell(8, 8).north = 'secret'

    # create view, attach, and display map
    view = TarjanView()
    view.map = map
    map.attach(view)
    view.update(0, 0, map.width, map.height)
    
    

